package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"html/template"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"os/signal"
	"os/user"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"syscall"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gobuffalo/packr"
	"github.com/syyongx/php2go"
)

type Job struct {
	Id       int
	Name     string
	Open     bool   `json:"open"`
	Command  string `json:"command"`
	Killby   string `json:"killby"`
	Owner    string `json:"owner"`
	Group    string `json:"group"`
	Log      string `json:"log"`
	Timer    string `json:"timer"`
	Restart  bool   `json:"restart"`
	Maxlife  int    `json:"maxlife"`
	Desc     string `json:"desc"`
	Loadtime string
	Active   bool
	Runtime  string
	Pid      int
	Procnum  int
	State    string
	Updated  bool
	Droped   bool
	Show     bool
	Error    string
	Lastexec int
	Jumptime string
}

// TimerResult Timer解析结果
type TimerResult struct {
	WeekDays []int
	Monthes  []int
	Days     []int
	Hours    []int
	Minutes  []int
}

var (
	jobs map[string]*Job
	conf map[string]int

	_author  = "dalu"
	_version = "0.0.8"
	_email   = "guodalu@qq.com"
	_update  = "2023-03-20"
)

func main() {

	if runtime.GOOS == "windows" {
		fmt.Println("ERROR: 当前程序仅支持在Linux上运行.")
		return
	}

	//忽略回收子进程资源信号，子进程执行完退出后，资源交由init进程清理
	if runtime.GOOS == "linux" {
		signal.Ignore(syscall.SIGCHLD)
	}

	// 定义参数
	config := flag.String("config", "crontab.json", "配置文件或目录路径,文件名须以.json结尾")
	port := flag.Int("port", 9501, "Web控制台监听端口")
	parse := flag.Bool("parse", false, "不启动服务,仅测试解析配置文件")
	version := flag.Bool("version", false, "输出当前版本")
	flag.Parse()

	if *version {
		fmt.Println("当前版本:", _version)
		return
	}

	log.Println("[NOTICE] 正在载入配置")

	jobs = map[string]*Job{}
	conf = map[string]int{}
	jobs, conf, err := ParseConfig(*config)

	if err != nil {
		log.Println("[ERROR]", err.Error())
		return
	}

	for k, _ := range conf {
		log.Println("[NOTICE] 配置解析通过=>", k)
	}

	//仅解析
	if *parse {
		return
	}

	log.Println("[NOTICE] 正在启动服务")

	//开启goroutine
	for k, v := range jobs {
		v.Id = NewJobId()
		v.Name = strings.TrimSpace(k)
		v.Loadtime = time.Now().Format("2006-01-02 15:04")
		v.Droped = false
		v.Error = ""
		v.Lastexec = 0
		go Run(v)
	}
	gin.SetMode(gin.ReleaseMode)
	gin.DefaultWriter = ioutil.Discard
	r := gin.Default()
	box := packr.NewBox("./js")
	r.StaticFS("/js", box)
	r.GET("/", func(c *gin.Context) {
		action := strings.TrimSpace(c.Query("action"))
		name := strings.TrimSpace(c.Query("name"))
		refresh := strings.TrimSpace(c.DefaultQuery("refresh", "0"))
		group := strings.TrimSpace(c.Query("group"))
		//标记job项是否有增减
		jobChanged := false
		newJobs := []string{}
		delJobs := []string{}
		//解析最新配置项
		resJobs, _, err := ParseConfig(*config)
		errstr := ""
		if err == nil {
			//计算新增的
			for _k, _v := range resJobs {
				if _job, ok := jobs[_k]; ok {
					_job.Updated = _job.Open != _v.Open || _job.Command != _v.Command || _job.Owner != _v.Owner ||
						_job.Log != _v.Log || _job.Maxlife != _v.Maxlife || _job.Killby != _v.Killby ||
						_job.Timer != _v.Timer || _job.Restart != _v.Restart || _job.Desc != _v.Desc
				} else {
					jobChanged = true
					newJobs = append(newJobs, _k)
				}
			}
			//计算已删除的
			for _k, _ := range jobs {
				if _, ok := resJobs[_k]; !ok {
					jobChanged = true
					delJobs = append(delJobs, _k)
				}
			}
		} else {
			errstr = err.Error()
		}
		//请求：重载配置项
		if action == "reload" {
			if err != nil {
				c.JSON(200, gin.H{"status": false, "message": "无法重载, 配置项解析异常, " + err.Error()})
				return
			}
			//单个项目重载
			if name != "" {
				oldJob, ok1 := jobs[name]
				newJob, ok2 := resJobs[name]
				if !ok1 || !ok2 {
					c.JSON(200, gin.H{"status": false, "message": "要重载的配置项不存在"})
					return
				}
				if !oldJob.Updated {
					c.JSON(200, gin.H{"status": false, "message": "请求的配置项无需重载"})
					return
				}
				if ProcExists(oldJob) {
					_ = KillProc(oldJob)
					log.Println("[NOTICE]", oldJob.Name, "任务已杀死")
				}
				oldJob.Open = false
				oldJob.Command = newJob.Command
				oldJob.Killby = newJob.Killby
				oldJob.Owner = newJob.Owner
				oldJob.Group = newJob.Group
				oldJob.Log = newJob.Log
				oldJob.Timer = newJob.Timer
				oldJob.Restart = newJob.Restart
				oldJob.Maxlife = newJob.Maxlife
				oldJob.Desc = newJob.Desc
				oldJob.Loadtime = time.Now().Format("2006-01-02 15:04")
				oldJob.Runtime = ""
				oldJob.Pid = 0
				oldJob.Procnum = 0
				oldJob.State = "normal"
				oldJob.Updated = false
				oldJob.Droped = false
				oldJob.Error = ""
				oldJob.Lastexec = 0
				oldJob.Active = false
				oldJob.Open = newJob.Open
				oldJob.Jumptime = time.Now().Format("2006-01-02 15:04")
				c.JSON(200, gin.H{"status": true, "message": "配置项重载成功"})
				return
			}
			if !jobChanged {
				c.JSON(200, gin.H{"status": false, "message": "没有配置项变动"})
				return
			}
			if len(newJobs) > 0 {
				for _, v := range newJobs {
					if nj, ok := resJobs[v]; ok {
						nj.Id = NewJobId()
						nj.Name = v
						nj.Loadtime = time.Now().Format("2006-01-02 15:04")
						nj.Droped = false
						nj.Error = ""
						nj.Lastexec = 0
						nj.Jumptime = time.Now().Format("2006-01-02 15:04")
						jobs[v] = nj
						go Run(nj)
					}
				}
			}
			if len(delJobs) > 0 {
				for _, v := range delJobs {
					if oj, ok := jobs[v]; ok {
						oj.Open = false
						if ProcExists(oj) {
							_ = KillProc(oj)
							log.Println("[NOTICE]", oj.Name, "任务已杀死")
						}
						oj.Droped = true
						delete(jobs, v)
					}
				}
			}
			c.JSON(200, gin.H{"status": true, "message": "配置项已变更"})
			return
		}
		//请求：打开关闭任务
		if php2go.InArray(action, []string{"open", "close"}) {
			if name == "" {
				c.JSON(200, gin.H{"status": false, "message": "缺少配置项名称"})
				return
			}
			_job, ok := jobs[name]
			if !ok {
				c.JSON(200, gin.H{"status": false, "message": "操作的配置项不存在"})
				return
			}
			switch action {
			case "open":
				_job.Open = true
				_job.Procnum = ProcNum(_job)
				_job.Active = _job.Procnum > 0
				log.Println("[NOTICE]", _job.Name, "任务已开启")
			case "close":
				if ProcExists(_job) {
					KillProc(_job)
					log.Println("[NOTICE]", _job.Name, "任务已杀死")
				}
				_job.Open = false
				_job.Procnum = ProcNum(_job)
				_job.Active = _job.Procnum > 0
				log.Println("[NOTICE]", _job.Name, "任务已关闭")
			}
			c.JSON(200, gin.H{"status": true, "message": "状态操作完成"})
			return
		}
		//请求：运行结束任务
		if php2go.InArray(action, []string{"killproc", "runjob"}) {
			if name == "" {
				c.JSON(200, gin.H{"status": false, "message": "缺少配置项名称"})
				return
			}
			_job, ok := jobs[name]
			if !ok {
				c.JSON(200, gin.H{"status": false, "message": "操作的配置项不存在"})
				return
			}
			switch action {
			case "killproc":
				if ProcExists(_job) {
					KillProc(_job)
					log.Println("[NOTICE]", _job.Name, "任务已杀死")
				}
				_job.Procnum = ProcNum(_job)
				_job.Active = _job.Procnum > 0
				c.JSON(200, gin.H{"status": true, "message": "任务已杀死"})
				return
			case "runjob":
				if !ProcExists(_job) {
					output := GetLogPath(_job)
					pid, err := ExecCommand(_job.Command, _job.Owner, output)
					if err != nil {
						_job.State = "error"
						_job.Error = "job运行失败, " + err.Error()
						c.JSON(200, gin.H{"status": false, "message": "job运行失败," + err.Error()})
						return
					}
					_job.Pid = pid
					_job.Procnum = ProcNum(_job)
					_job.Active = _job.Procnum > 0
					_job.Runtime = time.Now().Format("2006-01-02 15:04")
					_job.Lastexec = int(time.Now().Unix())
					_job.Jumptime = time.Now().Format("2006-01-02 15:04")
					log.Println("[NOTICE]", _job.Name, "任务执行成功,进程号["+strconv.Itoa(pid)+"]")
				}
				c.JSON(200, gin.H{"status": true, "message": "任务已运行"})
				return
			}
		}
		//请求：获取最后10行日志
		if action == "getlog" {
			if name == "" {
				c.Header("Content-Type", "text/html; charset=utf-8")
				c.String(200, `<p style="color:red">ERROR: 无法获取日志, 未指定job名称</p>`)
				return
			}
			line := strings.TrimSpace(c.DefaultQuery("line", "10"))
			_job, ok := jobs[name]
			if !ok {
				c.Header("Content-Type", "text/html; charset=utf-8")
				c.String(200, `<p style="color:red">ERROR: 无法获取日志, 指定的job名称未找到</p>`)
				return
			}
			if !IsFile(_job.Log) {
				c.Header("Content-Type", "text/html; charset=utf-8")
				c.String(200, `<p style="color:red">ERROR: 无法获取日志, 该job日志文件不存在</p>`)
				return
			}
			tailcmd := "tail -n " + line + " " + _job.Log
			res, _ := exec.Command("/bin/bash", "-c", tailcmd).Output()
			_res := strings.TrimSpace(string(res))
			_res = strings.Replace(_res, "\n", `<br>`, -1)
			f, err := os.Stat(_job.Log)
			filemt := ""
			if err == nil {
				filemt = f.ModTime().Format("2006-01-02 15:04")
			}
			data := gin.H{
				"job":     _job,
				"filemt":  filemt,
				"content": template.HTML(_res),
				"author":  _author,
				"version": _version,
				"email":   _email,
				"update":  _update,
			}
			r.SetHTMLTemplate(GetStaticRes("log.html"))
			c.HTML(200, "log.html", data)
			return
		}
		groups := []string{}
		//默认请求：过滤分组
		for _, v := range jobs {
			v.Show = true
			if group != "" && group != v.Group {
				v.Show = false
			}
			if v.Group != "" && !php2go.InArray(v.Group, groups) {
				groups = append(groups, v.Group)
			}
		}
		data := gin.H{
			"jobs":        jobs,
			"job_changed": jobChanged,
			"author":      _author,
			"version":     _version,
			"email":       _email,
			"refresh":     refresh,
			"group":       group,
			"groups":      groups,
			"update":      _update,
			"errstr":      errstr,
		}
		r.SetHTMLTemplate(GetStaticRes("index.html"))
		c.HTML(200, "index.html", data)

	})
	r.Run(":" + strconv.Itoa(*port))
}

// 校验job配置项格式
func VerifyJob(job *Job) error {
	//命令检查
	cmd := strings.TrimSpace(job.Command)
	if cmd == "" {
		return errors.New("配置项[Command]不能为空")
	}
	//用户检查
	owner := strings.TrimSpace(job.Owner)
	if owner == "" {
		return errors.New("配置项[Owner]不能为空")
	}
	//校验Timer
	timer := php2go.Explode(" ", job.Timer)
	//
	if len(timer) < 5 {
		return errors.New("配置项[Timer]格式异常")
	}
	//
	reg, _ := regexp.Compile(`[^\*\d\-\,\/]`)
	for _, v := range timer {
		matched := reg.Match([]byte(v))
		if matched {
			return errors.New("配置项[Timer]包含非法字符")
		}
	}
	_, err := ParseTimer(timer)
	if err != nil {
		return err
	}
	return nil
}

// 解析配置文件
func ParseConfig(path string) (map[string]*Job, map[string]int, error) {
	resJobs := map[string]*Job{}
	resConf := map[string]int{}
	if IsDir(path) {
		files, err := os.ReadDir(path)
		if err != nil {
			return nil, nil, errors.New("目录地址无效, " + err.Error())
		}
		for _, file := range files {
			_jobs := map[string]*Job{}
			if !file.IsDir() && strings.HasSuffix(file.Name(), ".json") {
				fullpath := php2go.Rtrim(path, "/") + "/" + file.Name()
				b, err := os.ReadFile(fullpath)
				if err != nil {
					return nil, nil, errors.New("配置解析错误, " + file.Name() + " => " + err.Error())
				}
				if err := json.Unmarshal(b, &_jobs); err != nil {
					return nil, nil, errors.New("配置解析错误, " + file.Name() + " => " + err.Error())
				}
				for k, v := range _jobs {
					_, ok := resJobs[k]
					if ok {
						return nil, nil, errors.New("配置解析错误, " + file.Name() + " => job[" + k + "]重复定义")
					}
					err := VerifyJob(v)
					if err != nil {
						return nil, nil, errors.New("配置解析错误, " + file.Name() + " => job[" + k + "] => " + err.Error())
					}
					resJobs[k] = v
				}
				f, _ := os.Open(fullpath)
				t, _ := f.Stat()
				resConf[fullpath] = int(t.ModTime().Unix())
				f.Close()
			}
		}
	} else if IsFile(path) {
		b, err := os.ReadFile(path)
		if err != nil {
			return nil, nil, errors.New("配置无法读取, " + path + " => " + err.Error())
		}
		if err := json.Unmarshal(b, &resJobs); err != nil {
			return nil, nil, errors.New("配置无法解析, " + path + " => " + err.Error())
		}
		for k, v := range resJobs {
			err := VerifyJob(v)
			if err != nil {
				return nil, nil, errors.New("配置解析错误, " + path + " => job[" + k + "] => " + err.Error())
			}
		}
		f, _ := os.Open(path)
		t, _ := f.Stat()
		resConf[path] = int(t.ModTime().Unix())
		f.Close()
	} else {
		return nil, nil, errors.New("无效的配置文件参数")
	}
	if len(resJobs) == 0 {
		return nil, nil, errors.New("未找到有效的配置项")
	}
	return resJobs, resConf, nil
}

// 获取静态文件资源模板
func GetStaticRes(file string) *template.Template {
	box := packr.NewBox("./template")
	str, _ := box.FindString(file)
	t, _ := template.New("").Parse(str)
	return t
}

// run job
func Run(job *Job) {

	log.Println("[NOTICE]", job.Name, "任务线程已就绪")

	for {

		if job.Droped {
			break
		}

		job.State = "normal"

		// 任务状态检查
		if !job.Open {
			job.State = "closed"
			time.Sleep(time.Minute)
			continue
		}

		job.Jumptime = time.Now().Format("2006-01-02 15:04")

		// 检查job.Timer定义是否合法
		timer := php2go.Explode(" ", job.Timer)
		// 判断当前时间点是否允许执行当前任务
		isok, err := IsAllowRunning(timer, job.Lastexec)
		if !isok {
			if err != nil {
				log.Println("[ERROR]", job.Name, err.Error())
				job.State = "error"
				job.Error = err.Error()
				job.Open = false
			}
			job.Procnum = ProcNum(job)
			job.Active = job.Procnum > 0
			time.Sleep(time.Minute)
			continue
		}
		//程序输出目录检测
		output := GetLogPath(job)
		// 判断每次执行时，之前的进程是否要删除
		needRestart := job.Restart
		if job.Maxlife > 0 && int(time.Now().Unix())-job.Lastexec >= job.Maxlife {
			needRestart = true
		}
		if needRestart && ProcExists(job) {
			_ = KillProc(job)
			log.Println("[NOTICE]", job.Name, "进程已结束")
		}
		//准备执行
		if !ProcExists(job) {
			pid, err := ExecCommand(job.Command, job.Owner, output)
			if err != nil {
				log.Println("[ERROR]", job.Name, "命令执行失败,", err.Error())
				job.State = "error"
				job.Error = "命令执行失败," + err.Error()
				job.Open = false
				continue
			}
			job.Runtime = time.Now().Format("2006-01-02 15:04")
			job.Pid = pid
			job.Lastexec = int(time.Now().Unix())
			job.Procnum = ProcNum(job)
			job.Active = job.Procnum > 0
			log.Println("[NOTICE]", job.Name, "任务启动成功,进程号["+strconv.Itoa(pid)+"]")
		} else {
			job.Procnum = ProcNum(job)
			job.Active = job.Procnum > 0
			log.Println("[NOTICE]", job.Name, "任务正在执行,已忽略")
		}
		time.Sleep(time.Minute)

	}
}

// 获取最大jobID
func NewJobId() int {
	id := 0
	for _, v := range jobs {
		if v.Id > id {
			id = v.Id
		}
	}
	return id + 1
}

// 获取日志输出文件路径
func GetLogPath(job *Job) string {
	output := strings.TrimSpace(job.Log)
	if output != "" {
		if php2go.Strpos(output, "{y}", 0) >= 0 {
			output = php2go.StrReplace("{y}", strconv.Itoa(int(time.Now().Year())), output, -1)
		}
		if php2go.Strpos(output, "{m}", 0) >= 0 {
			output = php2go.StrReplace("{m}", strconv.Itoa(int(time.Now().Month())), output, -1)
		}
		if php2go.Strpos(output, "{d}", 0) >= 0 {
			output = php2go.StrReplace("{d}", strconv.Itoa(int(time.Now().Day())), output, -1)
		}
		if php2go.Strpos(output, "{h}", 0) >= 0 {
			output = php2go.StrReplace("{h}", strconv.Itoa(int(time.Now().Hour())), output, -1)
		}
		dir := filepath.Dir(output)
		if !IsDir(dir) {
			err := os.MkdirAll(dir, 0755)
			if err != nil {
				output = "/tmp/" + job.Name + ".log"
				log.Println("[WARNING]", job.Name, "配置项[log]路径异常, 日志将重定向到", output, err)
			}
		}
	}
	return output
}

// 根据命令名称删除进程
func KillProc(job *Job) error {
	command := job.Command
	killby := strings.TrimSpace(job.Killby)
	if killby != "" {
		parts := php2go.Explode(",", killby)
		pscmd := "ps -ef | grep -v grep "
		for _, v := range parts {
			v := strings.TrimSpace(v)
			pscmd = pscmd + "| grep \"" + Addslashes(v) + "\" "
		}
		pscmd = pscmd + "| awk '{print $2}' | xargs kill -9"
		err := exec.Command("/bin/bash", "-c", pscmd).Run()
		if err != nil {
			return err
		}
	}
	parts := php2go.Explode(" ", command)
	pscmd := "ps -ef | grep -v grep "
	for _, v := range parts {
		v := strings.TrimSpace(v)
		pscmd = pscmd + "| grep \"" + Addslashes(v) + "\" "
	}
	pscmd = pscmd + "| awk '{print $2}' | xargs kill -9"
	err := exec.Command("/bin/bash", "-c", pscmd).Run()
	return err
}

// 进程是否存在
func ProcExists(job *Job) bool {
	return ProcNum(job) > 0
}

// 当前进程数量
func ProcNum(job *Job) int {
	command := job.Command
	killby := strings.TrimSpace(job.Killby)
	var res []byte
	var parts []string
	if killby == "" {
		parts = php2go.Explode(" ", command)
	} else {
		parts = php2go.Explode(",", killby)
	}
	pscmd := "ps -ef | grep -v grep "
	for _, v := range parts {
		v := strings.TrimSpace(v)
		pscmd = pscmd + "| grep \"" + Addslashes(v) + "\" "
	}
	pscmd = pscmd + "| wc -l"
	res, _ = exec.Command("/bin/bash", "-c", pscmd).Output()
	c := 0
	_res := strings.TrimSpace(string(res))
	if _res != "" {
		c, _ = strconv.Atoi(_res)
	}
	return c
}

// 为命令添加转义符
func Addslashes(str string) string {
	var buf bytes.Buffer
	for _, char := range str {
		switch char {
		case '\'', '"', '\\', '-', '&', '|', '>', '<', '*', '?', ';', '{', '}', '[', ']', ' ':
			buf.WriteRune('\\')
		}
		buf.WriteRune(char)
	}
	return buf.String()
}

// 执行系统命令
func ExecCommand(command, owner, outfile string) (int, error) {
	parts := php2go.Explode(" ", command)
	cmd := exec.Command(parts[0], parts[1:]...)
	if outfile != "" {
		f, _ := os.OpenFile(outfile, os.O_WRONLY|os.O_CREATE|os.O_SYNC|os.O_APPEND, 0755)
		//重定向输出
		cmd.Stdout = f
		cmd.Stderr = f
		defer f.Close()
	}
	//设置进程组pgid
	cmd.SysProcAttr = &syscall.SysProcAttr{}
	cmd.SysProcAttr.Setpgid = true

	_user, err := user.Current()
	if err != nil {
		return 0, errors.New("当前系统用户获取失败")
	}
	if _user.Username != owner {
		_execuser, err := user.Lookup(owner)
		if err != nil {
			return 0, errors.New("目标用户信息获取失败")
		}
		uid, _ := strconv.Atoi(_execuser.Uid)
		gid, _ := strconv.Atoi(_execuser.Gid)
		cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
	}
	err = cmd.Start()
	if err != nil {
		return 0, errors.New("脚本执行失败, " + err.Error())
	}
	//mac回收进程
	if runtime.GOOS == "darwin" {
		go func() {
			if err := cmd.Wait(); err == nil {
				log.Println("[NOTICE] 进程[" + strconv.Itoa(cmd.Process.Pid) + "]回收成功")
			}
		}()
	}
	return cmd.Process.Pid, nil
}

// 按范围创建int型切片
func RangeSlice(start, end, step int) []int {
	s := make([]int, 0, (end-start)/step)
	for i := start; i < end; i += step {
		s = append(s, i)
	}
	return s
}

// 解析timer格式
func ParseTimer(timer []string) (TimerResult, error) {
	//
	result := TimerResult{}
	//校验周
	week := strings.TrimSpace(timer[4])
	if week != "*" {
		if php2go.Strpos(week, "/", 0) >= 0 {
			return result, errors.New("配置项[Timer]中[周]字段不能包含[/]")
		}
		weekDays := []int{}
		if php2go.Strpos(week, ",", 0) >= 0 {
			dayList := php2go.Explode(",", week)
			invalid := false
			for _, v := range dayList {
				v = strings.TrimSpace(v)
				d, err := strconv.Atoi(v)
				if err != nil {
					invalid = true
					break
				}
				if d < 0 || d > 6 {
					invalid = true
					break
				}
				if !php2go.InArray(d, weekDays) {
					weekDays = append(weekDays, d)
				}
			}
			if invalid {
				return result, errors.New("配置项[Timer]中[周]字段格式异常")
			}
		} else if php2go.Strpos(week, "-", 0) >= 0 {
			dayList := php2go.Explode("-", week)
			min, err1 := strconv.Atoi(strings.TrimSpace(dayList[0]))
			max, err2 := strconv.Atoi(strings.TrimSpace(dayList[1]))
			if err1 != nil || err2 != nil || min >= max || min < 0 || max > 6 {
				return result, errors.New("配置项[Timer]中[周]字段格式异常")
			}
			for i := min; i <= max; i++ {
				weekDays = append(weekDays, i)
			}
		} else {
			w, err := strconv.Atoi(week)
			if err != nil || w < 0 || w > 6 {
				return result, errors.New("配置项[Timer]中[月]字段格式异常")
			}
			weekDays = append(weekDays, w)
		}
		timer[3] = "*"
		timer[2] = "*"
		result.WeekDays = weekDays
	} else {
		result.WeekDays = RangeSlice(0, 7, 1)
	}

	//校验月
	month := strings.TrimSpace(timer[3])
	if month != "*" {
		if php2go.Strpos(month, "/", 0) >= 0 {
			return result, errors.New("配置项[Timer]中[月]字段不能包含[/]")
		}
		monthes := []int{}
		if php2go.Strpos(month, ",", 0) >= 0 {
			mlist := php2go.Explode(",", month)
			invalid := false
			for _, v := range mlist {
				v = strings.TrimSpace(v)
				m, err := strconv.Atoi(v)
				if err != nil {
					invalid = true
					break
				}
				if m < 1 || m > 12 {
					invalid = true
					break
				}
				if !php2go.InArray(m, monthes) {
					monthes = append(monthes, m)
				}
			}
			if invalid {
				return result, errors.New("配置项[Timer]中[月]字段格式异常")
			}
		} else if php2go.Strpos(month, "-", 0) >= 0 {
			mlist := php2go.Explode("-", month)
			min, err1 := strconv.Atoi(strings.TrimSpace(mlist[0]))
			max, err2 := strconv.Atoi(strings.TrimSpace(mlist[1]))
			if err1 != nil || err2 != nil || min >= max || min < 1 || max > 31 {
				return result, errors.New("配置项[Timer]中[月]字段格式异常")
			}
			for i := min; i <= max; i++ {
				monthes = append(monthes, i)
			}
		} else {
			m, err := strconv.Atoi(month)
			if err != nil || m < 1 || m > 12 {
				return result, errors.New("配置项[Timer]中[月]字段格式异常")
			}
			monthes = append(monthes, m)
		}
		result.Monthes = monthes
	} else {
		result.Monthes = RangeSlice(1, 13, 1)
	}

	//校验日
	day := strings.TrimSpace(timer[2])
	if day != "*" {
		days := []int{}
		if php2go.Strpos(day, "/", 0) >= 0 {
			conf := php2go.Explode("/", day)
			scope := strings.TrimSpace(conf[0])
			freq := strings.TrimSpace(conf[1])
			matched, err := regexp.Match(`[^\d\-\*]`, []byte(scope))
			if err != nil || matched {
				return result, errors.New("配置项[Timer]中[日]字段格式异常")
			}
			_days := []int{}
			if scope == "*" {
				for i := 1; i <= 31; i++ {
					_days = append(_days, i)
				}
			} else if php2go.Strpos(scope, "-", 0) >= 0 {
				sclist := php2go.Explode("-", scope)
				min, err1 := strconv.Atoi(strings.TrimSpace(sclist[0]))
				max, err2 := strconv.Atoi(strings.TrimSpace(sclist[1]))
				if err1 != nil || err2 != nil || min >= max || min < 1 || max > 31 {
					return result, errors.New("配置项[Timer]中[日]字段格式异常")
				}
				for i := min; i <= max; i++ {
					_days = append(_days, i)
				}
			}
			matched, err = regexp.Match(`^\d+$`, []byte(freq))
			if len(_days) == 0 || err != nil || !matched {
				return result, errors.New("配置项[Timer]中[日]字段格式异常")
			}
			i := 0
			_freq, _ := strconv.Atoi(freq)
			for _, v := range _days {
				if i == 0 || i%_freq == 0 {
					days = append(days, v)
				}
				i += 1
			}
		} else if php2go.Strpos(day, "-", 0) >= 0 {
			dlist := php2go.Explode("-", day)
			min, err1 := strconv.Atoi(strings.TrimSpace(dlist[0]))
			max, err2 := strconv.Atoi(strings.TrimSpace(dlist[1]))
			if err1 != nil || err2 != nil || min >= max || min < 1 || max > 31 {
				return result, errors.New("配置项[Timer]中[日]字段格式异常")
			}
			for i := min; i <= max; i++ {
				days = append(days, i)
			}
		} else if php2go.Strpos(day, ",", 0) >= 0 {
			dlist := php2go.Explode(",", day)
			invalid := false
			for _, v := range dlist {
				v = strings.TrimSpace(v)
				d, err := strconv.Atoi(v)
				if err != nil {
					invalid = true
					break
				}
				if d < 1 || d > 31 {
					invalid = true
					break
				}
				if !php2go.InArray(d, days) {
					days = append(days, d)
				}
			}
			if invalid {
				return result, errors.New("配置项[Timer]中[日]字段格式异常")
			}
		} else {
			d, err := strconv.Atoi(day)
			if err != nil || d < 1 || d > 31 {
				return result, errors.New("配置项[Timer]中[日]字段格式异常")
			}
			days = append(days, d)
		}
		result.Days = days
	} else {
		result.Days = RangeSlice(1, 32, 1)
	}

	//校验时
	hour := strings.TrimSpace(timer[1])
	if hour != "*" {
		hours := []int{}
		if php2go.Strpos(hour, "/", 0) >= 0 {
			conf := php2go.Explode("/", hour)
			scope := strings.TrimSpace(conf[0])
			freq := strings.TrimSpace(conf[1])
			matched, err := regexp.Match(`[^\d\-\*]`, []byte(scope))
			if err != nil || matched {
				return result, errors.New("配置项[Timer]中[时]字段格式异常")
			}
			_hours := []int{}
			if scope == "*" {
				for i := 0; i <= 23; i++ {
					_hours = append(_hours, i)
				}
			} else if php2go.Strpos(scope, "-", 0) >= 0 {
				sclist := php2go.Explode("-", scope)
				min, err1 := strconv.Atoi(strings.TrimSpace(sclist[0]))
				max, err2 := strconv.Atoi(strings.TrimSpace(sclist[1]))
				if err1 != nil || err2 != nil || min >= max || min < 0 || max > 23 {
					return result, errors.New("配置项[Timer]中[时]字段格式异常")
				}
				for i := min; i <= max; i++ {
					_hours = append(_hours, i)
				}
			}
			matched, err = regexp.Match(`^\d+$`, []byte(freq))
			if len(_hours) == 0 || err != nil || !matched {
				return result, errors.New("配置项[Timer]中[时]字段格式异常")
			}
			i := 0
			_freq, _ := strconv.Atoi(freq)
			for _, v := range _hours {
				if i == 0 || i%_freq == 0 {
					hours = append(hours, v)
				}
				i += 1
			}
		} else if php2go.Strpos(hour, "-", 0) >= 0 {
			hlist := php2go.Explode("-", hour)
			min, err1 := strconv.Atoi(strings.TrimSpace(hlist[0]))
			max, err2 := strconv.Atoi(strings.TrimSpace(hlist[1]))
			if err1 != nil || err2 != nil || min >= max || min < 0 || max > 23 {
				return result, errors.New("配置项[Timer]中[时]字段格式异常")
			}
			for i := min; i <= max; i++ {
				hours = append(hours, i)
			}
		} else if php2go.Strpos(hour, ",", 0) >= 0 {
			hlist := php2go.Explode(",", hour)
			invalid := false
			for _, v := range hlist {
				v = strings.TrimSpace(v)
				h, err := strconv.Atoi(v)
				if err != nil {
					invalid = true
					break
				}
				if h < 0 || h > 23 {
					invalid = true
					break
				}
				if !php2go.InArray(h, hours) {
					hours = append(hours, h)
				}
			}
			if invalid {
				return result, errors.New("配置项[Timer]中[时]字段格式异常")
			}
		} else {
			h, err := strconv.Atoi(hour)
			if err != nil || h < 0 || h > 23 {
				return result, errors.New("配置项[Timer]中[时]字段格式异常")
			}
			hours = append(hours, h)
		}
		result.Hours = hours
	} else {
		result.Hours = RangeSlice(0, 24, 1)
	}

	//按分钟校验
	min := strings.TrimSpace(timer[0])
	if min != "*" {
		minutes := []int{}
		if php2go.Strpos(min, "/", 0) >= 0 {
			conf := php2go.Explode("/", min)
			scope := strings.TrimSpace(conf[0])
			freq := strings.TrimSpace(conf[1])
			matched, err := regexp.Match(`[^\d\-\*]`, []byte(scope))
			if err != nil || matched {
				return result, errors.New("配置项[Timer]中[分]字段格式异常")
			}
			_minutes := []int{}
			if scope == "*" {
				for i := 0; i <= 59; i++ {
					_minutes = append(_minutes, i)
				}
			} else if php2go.Strpos(scope, "-", 0) >= 0 {
				sclist := php2go.Explode("-", scope)
				min, err1 := strconv.Atoi(strings.TrimSpace(sclist[0]))
				max, err2 := strconv.Atoi(strings.TrimSpace(sclist[1]))
				if err1 != nil || err2 != nil || min >= max || min < 0 || max > 59 {
					return result, errors.New("配置项[Timer]中[分]字段格式异常")
				}
				for i := min; i <= max; i++ {
					_minutes = append(_minutes, i)
				}
			}
			matched, err = regexp.Match(`^\d+$`, []byte(freq))
			if len(_minutes) == 0 || err != nil || !matched {
				return result, errors.New("配置项[Timer]中[分]字段格式异常")
			}
			i := 0
			_freq, _ := strconv.Atoi(freq)
			for _, v := range _minutes {
				if i == 0 || i%_freq == 0 {
					minutes = append(minutes, v)
				}
				i += 1
			}
		} else if php2go.Strpos(min, "-", 0) >= 0 {
			mlist := php2go.Explode("-", min)
			min, err1 := strconv.Atoi(strings.TrimSpace(mlist[0]))
			max, err2 := strconv.Atoi(strings.TrimSpace(mlist[1]))
			if err1 != nil || err2 != nil || min >= max || min < 0 || max > 59 {
				return result, errors.New("配置项[Timer]中[分]字段格式异常")
			}
			for i := min; i <= max; i++ {
				minutes = append(minutes, i)
			}
		} else if php2go.Strpos(min, ",", 0) >= 0 {
			mlist := php2go.Explode(",", min)
			invalid := false
			for _, v := range mlist {
				v = strings.TrimSpace(v)
				m, err := strconv.Atoi(v)
				if err != nil {
					invalid = true
					break
				}
				if m < 0 || m > 59 {
					invalid = true
					break
				}
				if !php2go.InArray(m, minutes) {
					minutes = append(minutes, m)
				}
			}
			if invalid {
				return result, errors.New("配置项[Timer]中[分]字段格式异常")
			}
		} else {
			m, err := strconv.Atoi(min)
			if err != nil || m < 0 || m > 59 {
				return result, errors.New("配置项[Timer]中[分]字段格式异常")
			}
			minutes = append(minutes, m)
		}
		result.Minutes = minutes
	} else {
		result.Minutes = RangeSlice(0, 60, 1)
	}
	return result, nil
}

// 检查当前是否允许执行
func IsAllowRunning(timer []string, lastexec int) (bool, error) {
	result, err := ParseTimer(timer)
	if err != nil {
		return false, err
	}
	// 是否满足当周运行
	runInCurrWeekDay := php2go.InArray(int(time.Now().Weekday()), result.WeekDays)
	// 是否满足当月运行
	runInCurrMonth := php2go.InArray(int(time.Now().Month()), result.Monthes)
	// 是否满足本日运行
	runInToday := php2go.InArray(int(time.Now().Day()), result.Days)
	// 是否满足小时运行
	runInCurrHour := php2go.InArray(int(time.Now().Hour()), result.Hours)
	// 计算是否满足当前分钟
	runInCurrMin := php2go.InArray(int(time.Now().Minute()), result.Minutes)
	//
	allow := runInCurrWeekDay && runInCurrMonth && runInToday && runInCurrHour && runInCurrMin
	// 判断是否符合运行条件
	return allow && int(time.Now().Unix())-lastexec >= 60, nil

}

// 判断文件是否存在
func IsFile(file string) bool {
	_, err := os.Stat(file)
	if err != nil {
		return os.IsExist(err)
	}
	return true
}

// 判断目录是否存在
func IsDir(path string) bool {
	s, err := os.Stat(path)
	if err != nil {
		return false
	}
	return s.IsDir()
}
